/**
 * 200818：初版
 * 200821：改进map属性的getter和setter以与其它材质保持一致，不再需要赋空值来释放贴图
 * 200825：增加序列图相关功能及旋转功能
 * 201013：默认值处理
 * 210318：更改exposure为控制渐变对比度，exposureRatio为仅控制亮度
 * 20220304:[require] to [import]...jordia
 */
import {ShaderMaterial,Material,Vector2,LinearFilter,RepeatWrapping,ShaderLib,UniformsUtils,UnsignedByteType} from 'three';
import {GPUComputationRenderer} from "three/examples/jsm/misc/GPUComputationRenderer";

const MAX_STEPS=128;
const NUMBER_TIME=(n,m)=>{
    let nu=n;
    let t=0;
    do{
        nu-=m;
        t++;
    }while (nu>0);
    return t-1;
};
const QUOTIENT=new Float32Array((()=>{
    let qs=[];
    for(let i=0;i<MAX_STEPS;i++){
        let v=1/(i+1);
        qs.push(v);
    }
    return qs;
})());

/**
 * 模糊镜头效果材质
 * 分辨率对性能影响极大，够用即可
 *
 * 通用属性：MeshBasicMaterial的所有属性
 *  通过uniforms.map.value获取到的是渲染处理后的贴图
 *
 * 特有属性：
 *  blur:{value:0},模糊强度（0-1，一般0.2左右可模糊成纯色）
 *  scale:{value:2},缩放，值增加为缩小，值减少为放大
 *  offset:{value:new Vector2(0,0)},偏移值增加对应负方向上的偏移
 *  exposure:{value:2},曝光度，二次方球形分布
 *  exposeRatio:{value:0.05},整体亮度
 *  fastMode:{value:false},快速模式，模糊方向减少为4个，速度更快，效果更差
 *  spread:{value:3},模糊计算时半径扩散的倍率（越大，效率越高，效果越差）
 *  step:{value:0.001},模糊计算时的基础半径（越大，效率越高，效果越差）
 *  index:{value:0},要显示的序列编号
 *  cell:{value:new Vector2(1,1)},序列图单元格尺寸
 *  angle:{value:0},旋转角度（弧度）
 *
 *  特有属性更改后需调用update方法后才生效
 *
 *  伪代码示例：
 *      let tex=texture;
 *      let g=new CircleBufferGeometry(0.5,36);
 *      let m=new MicroscopeImageMaterial(renderer,{
 *            map:tex,
 *            diffuse:new Color('#ffd89d'),
 *            cell:new Vector2(0.25,0.25),
 *       },350,350);
 *      let mm=new Mesh(g,m);
 *      scene.add(mm);
 *      m.fastMode=false;
 *      m.step=0.001;
 *      m.spread=2;
 *      m.scale=1;
 *      m.offset.x=0;
 *      m.exposure=0.5;
 *      m.exposeRatio=0.5;
 *      m.index=8;
 *      m.blur=0.002;
 *      m.update(0);
 *
 *      let v=0;
 *      let ani=()=>{
 *          v+=0.006;
 *          m.blur=pingPong(0,0.05,v);
 *          m.offset.x=pingPong(0,0.1,v);
 *          m.scale=pingPong(0.5,2,v);
 *          m.exposure=pingPong(1.5,4,v);
 *          m.exposeRatio=pingPong(0.01,0.1,v);
 *          m.index=pingPong(0,15,v);
 *          m.angle=pingPong(0,5,v);
 *          m.update(5);
 *          requestAnimationFrame(ani);
 *      };
 *      ani();
 */
export default class MicroscopeImageMaterial extends ShaderMaterial{
    constructor(renderer,values={},resolutionX=256,resolutionY=256){
        super();
        this._renderer=renderer;
        this._values=values;
        this._resolutionX=resolutionX;
        this._resolutionY=resolutionY;
        this.init();
    }

    //------------------------------------------对外属性及方法-----------------------------------------

    /**
     * 属性赋值
     * @param values {object} meshBasicMaterial所支持的属性及此材质特有的属性
     */
    set values(values){
        for(let key in values){
            if(this.hasOwnProperty(''+key+'')){
                if(values[''+key+'']!==undefined){
                    this[''+key+'']=values[''+key+''];
                }
            }else{
                console.warn(''+key+' is not a property of MicroscopeImageMaterial!');
            }
        }
        this.needsUpdate=true;
    }

    /**
     * 获取属性对象
     * @returns {*}
     */
    get values(){
        return this._paras;
    }

    /**
     * 渲染并更新材质贴图（仅用于更新特有材质属性）
     * @param steps {Number} 最大128，模糊次数越多，效果越好，性能越差，<=0时按设定步长和倍率计算取样次数
     */
    update(steps=5){
        if(!this.renderTrsMap())return;
        let ss=Math.min(steps,MAX_STEPS);
        let dta=1;
        let counter=0;
        if(ss<=0){
            let br=this.step;
            do{
                br*=this.spread;
                br=Math.min(br,this.blur);
                counter++;
                counter%=2;
                if(br>this._threshold*dta){
                    dta=NUMBER_TIME(br,this._threshold);
                    this.renderBlurMap(dta*this._threshold,counter===0);
                    dta++;
                }
            }while (br<this.blur);
            return;
        }
        let ub=this.blur*QUOTIENT[ss-1];
        for(let i=0;i<ss;i++) {
            let br=ub*(i+1);
            counter++;
            counter %= 2;
            if(br>this._threshold*dta){
                dta=NUMBER_TIME(br,this._threshold);
                this.renderBlurMap(dta*this._threshold, counter === 0);
                dta++;
            }
        }
    }

    /**
     * 拷贝
     * @param source
     * @returns {MicroscopeImageMaterial}
     */
    copy(source){
        Material.prototype.copy.call( this, source );
        this.values=source.values;
        return this;
    }

    /**
     * 克隆
     * @returns {MicroscopeImageMaterial}
     */
    clone() {
        let c=new MicroscopeImageMaterial();
        c.copy(this);
        return c;
    }

    /**
     * 销毁
     */
    dispose() {
        super.dispose();
        for(let rt of this._trsMap.renderTargets){
            rt.dispose();
        }
        this._trsMap=null;
        for(let rt of this._blurMap.renderTargets){
            rt.dispose();
        }
        this._blurMap=null;
        this._gpuCrTrs=null;
        this._gpuCrBlur=null;
        this._renderer=null;
        this._values=null;
        this._resolutionX=null;
        this._resolutionY=null;
        this._paras=null;
        this._exclusive=null;
        this.getter=null;
        this.setter=null;
    }

    //------------------------------------------私有属性及方法-------------------------------------------

    init(){
        this.initVariable();
        this.initGetter();
        this.initSetter();
        this.initProperty();
        this.initShaders();
        this.initUniforms();
        this.initGPUCR();
        this.initialize();
    }
    initVariable(){
        this._paras={};
        this._exclusive={
            spread:{value:3},
            step:{value:0.001},
            blur:{value:0},
            scale:{value:2},
            offset:{value:new Vector2(0,0)},
            exposure:{value:2},
            exposeRatio:{value:0.05},
            fastMode:{value:false},
            index:{value:0},
            cell:{value:new Vector2(1,1)},
            angle:{value:0},
        };
        this.getter={};
        this.setter={};
        this._threshold=1/Math.max(this._resolutionX,this._resolutionY);
    }
    initSetter(){
        this.setter.default=(key)=> {
            return ()=> {
                if (this.uniforms.hasOwnProperty(key)) {
                    return this.uniforms[key].value;
                }
                return this._paras[key];
            };
        };
        this.setter.offset=(key)=> {
            return ()=> {
                return this._trsMap.material.uniforms[key].value;
            };
        };
        this.setter.map=(key)=> {
            return ()=> {
                return this._paras[key];
            };
        };
    }
    initGetter() {
        this.getter.default = (key) => {
            return (v) => {
                if (this.uniforms.hasOwnProperty(key)) {
                    this.uniforms[key].value = v;
                }
                this._paras[key] = v;
            };
        };
        this.getter.map = (key) => {
            return (v) => {
                if (v) {
                    v.magFilter = LinearFilter;
                    v.minFilter = LinearFilter;
                    v.wrapS = RepeatWrapping;
                    v.wrapT = RepeatWrapping;
                }
                this.uniforms[key].value = v;
                this._paras[key] = v;
            };
        };
        this.getter.scale = (key) => {
            return (v) => {
                let _v= Math.max(v, 1e-6);
                this._trsMap.material.uniforms[key].value =_v;
                this._paras[key] = _v;
            };
        };
        this.getter.index = (key) => {
            return (v) => {
                let _v= Math.round(v).toFixed(0);
                this._trsMap.material.uniforms[key].value =_v;
                this._paras[key] = _v;
            };
        };
        this.getter.cell = (key) => {
            return (v) => {
                v.x=Math.max(Math.min(1,v.x),0);
                v.y=Math.max(Math.min(1,v.y),0);
                this._trsMap.material.uniforms[key].value.copy(v);
                this._paras[key] = v;
            };
        };
        this.getter.angle = (key) => {
            return (v) => {
                this._trsMap.material.uniforms[key].value=v;
                this._paras[key] = v;
            };
        };
        this.getter.offset = (key) => {
            return (v) => {
                this._trsMap.material.uniforms[key].value.copy(v);
                this._paras[key] = v;
            };
        };
        this.getter.fastMode = (key) => {
            return (v) => {
                this._blurMap.material.uniforms[key].value = v;
                this._paras[key] = v;
            };
        };
        this.getter.exposure = (key) => {
            return (v) => {
                this._trsMap.material.uniforms[key].value = v;
                this._paras[key] = v;
            };
        };
        this.getter.exposeRatio = (key) => {
            return (v) => {
                this._trsMap.material.uniforms[key].value = v;
                this._paras[key] = v;
            };
        };
        this.getter.spread = (key) => {
            return (v) => {
                this._paras[key] = Math.max(v,1);
            };
        };
    }
    getGetter(key){
        if(this.getter.hasOwnProperty(key)){
            return this.getter[key](key);
        }else{
            return this.getter.default(key);
        }
    }
    getSetter(key){
        if(this.setter.hasOwnProperty(key)){
            return this.setter[key](key);
        }else{
            return this.setter.default(key);
        }
    }
    definePropertyByObject(obj){
        for(let key of Reflect.ownKeys(obj)){
            if(key!=='constructor'&&key!=="prototype"&&key!=="name"&&key!=='id'&&key!=='uuid'){
                let ds=Object.getOwnPropertyDescriptor(obj,key);
                this._paras[key]=ds.value.value;
                let fc={
                    enumerable: true,
                    configurable: true,
                    set:this.getGetter(key),
                    get:this.getSetter(key)
                };
                Object.defineProperty(this,key,fc);
            }
        }
    }
    initProperty(){
        this.definePropertyByObject(ShaderLib.basic.uniforms);
        this.definePropertyByObject(this._exclusive);
        this.type='MicroscopeImageMaterial';
        this.clipping=true;
    }
    initShaders(){
        this.vertexShader=ShaderLib.basic.vertexShader;
        this.fragmentShader=ShaderLib.basic.fragmentShader;
    }
    initUniforms(){
        // this.uniforms=UniformsUtils.merge( [ShaderLib.basic.uniforms,this._exclusive] );
        this.uniforms=UniformsUtils.merge( [ShaderLib.basic.uniforms] );
        this._exclusive=null;
    }
    initGPUCR(){
        this._gpuCrBlur=new GPUComputationRenderer(this._resolutionX,this._resolutionY,this._renderer);
        this._gpuCrBlur.setDataType(UnsignedByteType);
        let blurMap=this._gpuCrBlur.createTexture();
        this._blurMap=this._gpuCrBlur.addVariable('blurMap',blurShader,blurMap);
        this._blurMap.material.uniforms.step={value:this._values.step||0.01};
        this._blurMap.material.uniforms.fastAxs={value:true};
        this._blurMap.material.uniforms.map={value:null};
        this._blurMap.material.uniforms.fastMode={value:this._values.fastMode!==undefined?this._values.fastMode:false};

        this._gpuCrTrs=new GPUComputationRenderer(this._resolutionX,this._resolutionY,this._renderer);
        this._gpuCrTrs.setDataType(UnsignedByteType);
        let trsMap=this._gpuCrTrs.createTexture();
        this._trsMap=this._gpuCrTrs.addVariable('trsMap',trsShader,trsMap);
        this._trsMap.material.uniforms.map={value:null};
        this._trsMap.material.uniforms.scale={value:this._values.scale||1};
        this._trsMap.material.uniforms.index={value:this._values.index!==undefined?this._values.index:0};
        this._trsMap.material.uniforms.cell={value:this._values.cell||new Vector2(1,1)};
        this._trsMap.material.uniforms.offset={value:this._values.offset||new Vector2(0.001,0)};
        this._trsMap.material.uniforms.exposure={value:this._values.exposure||2};
        this._trsMap.material.uniforms.exposeRatio={value:this._values.exposeRatio||0.05};
        this._trsMap.material.uniforms.angle={value:this._values.angle!==undefined?this._values.angle:0};

        let err=this._gpuCrBlur.init();
        err+=this._gpuCrTrs.init();
        if(err){
            console.error(err);
        }
    }
    renderTrsMap(){
        if(!this.map)return false;
        let renderUniform=this._trsMap.material.uniforms;
        renderUniform.map.value=this.map;
        this._gpuCrTrs.compute();
        this.uniforms.map.value=this._gpuCrTrs.getCurrentRenderTarget(this._trsMap).texture;
        return true;
    }
    renderBlurMap(step,fastAxs=true){
        let renderUniform=this._blurMap.material.uniforms;
        renderUniform.step.value=step;
        renderUniform.fastAxs.value=fastAxs;
        renderUniform.map.value=this.uniforms.map.value;
        this._gpuCrBlur.compute();
        this.uniforms.map.value=this._gpuCrBlur.getCurrentRenderTarget(this._blurMap).texture;
    }
    initialize(){
        this.values=this._values;
        this._values=null;
    }
}

const trsShader=[
    'uniform sampler2D map;' ,
    "uniform float scale;" ,
    "uniform vec2 offset;" ,
    'uniform float exposure;' ,
    'uniform float exposeRatio;' ,
    'uniform int index;' ,
    'uniform vec2 cell;' ,
    'uniform float angle;' ,

    'vec4 zone=vec4(0.);' ,
    'void getZone(int idx,vec2 size){' ,
    '   float lx=size.x*float(idx);' ,
    '   zone.x=mod(lx,1.);' ,
    '   zone.y=zone.x+size.x;' ,
    '   float cy=floor(lx)+1.;' ,
    '   zone.z=1.-mod(size.y*cy,1.);' ,
    '   zone.w=zone.z+size.y;' ,
    '}' ,
    'vec2 zUv(vec2 u,vec4 z){' ,
    '   vec2 re=u;' ,
    '   re.x=u.x*(z.y-z.x)+z.x;' ,
    '   re.y=u.y*(z.w-z.z)+z.z;' ,
    '   return re;' ,
    '}' ,
    'vec2 rotate( float ang, vec2 v ){',
    '   float s = ang==0.?0.:sin(ang);',
    '   float c = ang==0.?1.:cos(ang);',
    '   return vec2( c*v.x-s*v.y, s*v.x+c*v.y );',
    '}',

    'void main(){' ,
    '   vec2 vUv=gl_FragCoord.xy/resolution.xy;' ,

    '   getZone(index,cell);' ,
    '   vec2 tUv=rotate(angle,(vUv-0.5)*scale);' ,
    '   tUv.x=mod(tUv.x+offset.x+0.5,1.);' ,
    '   tUv.y=mod(tUv.y+offset.y+0.5,1.);' ,
    '   vec4 texelCol=texture2D(map,zUv(tUv,zone));' ,

    // '   float dis=0.70710678-length(vUv-0.5);' ,
    // '   dis*=4.*dis;' ,
    // '   texelCol*=dis*exposure;' ,
    // '   texelCol+=exposure*exposeRatio;' ,

    '   float dis=0.70710678-length(vUv-0.5);' ,
    '   dis=pow(dis,exposure);' ,
    '   texelCol*=dis;' ,
    '   texelCol*=exposeRatio;' ,

    '   gl_FragColor=texelCol;' ,
    '}'
].join('\n');

const blurShader=[
    'uniform float step;' ,
    'uniform bool fastAxs;' ,
    'uniform sampler2D map;' ,
    'uniform bool fastMode;' ,
    'vec4 blur(vec2 tuv,float fuzzy,sampler2D tex){' ,
    '   vec4 c=texture2D(tex,vec2(tuv.x,tuv.y))*0.147761;' ,
    '   c+=texture2D(tex,vec2(tuv.x+fuzzy,tuv.y))*0.118318;' ,
    '   c+=texture2D(tex,vec2(tuv.x,tuv.y-fuzzy))*0.118318;' ,
    '   c+=texture2D(tex,vec2(tuv.x-fuzzy,tuv.y))*0.118318;' ,
    '   c+=texture2D(tex,vec2(tuv.x,tuv.y+fuzzy))*0.118318;' ,
    '   c+=texture2D(tex,vec2(tuv.x+fuzzy,tuv.y+fuzzy))*0.0947416;' ,
    '   c+=texture2D(tex,vec2(tuv.x-fuzzy,tuv.y+fuzzy))*0.0947416;' ,
    '   c+=texture2D(tex,vec2(tuv.x+fuzzy,tuv.y-fuzzy))*0.0947416;' ,
    '   c+=texture2D(tex,vec2(tuv.x-fuzzy,tuv.y-fuzzy))*0.0947416;' ,
    '   return c;' ,
    '}' ,
    'vec4 blurFast(vec2 tuv,float fuzzy,sampler2D tex,bool axs){' ,
    '   vec4 c=texture2D(tex,vec2(tuv.x,tuv.y));' ,
    '   if(axs){' ,
    '       c+=texture2D(tex,vec2(tuv.x+fuzzy,tuv.y));' ,
    '       c+=texture2D(tex,vec2(tuv.x,tuv.y-fuzzy));' ,
    '       c+=texture2D(tex,vec2(tuv.x-fuzzy,tuv.y));' ,
    '       c+=texture2D(tex,vec2(tuv.x,tuv.y+fuzzy));' ,
    '   }else{' ,
    '       c+=texture2D(tex,vec2(tuv.x+fuzzy,tuv.y+fuzzy));' ,
    '       c+=texture2D(tex,vec2(tuv.x-fuzzy,tuv.y+fuzzy));' ,
    '       c+=texture2D(tex,vec2(tuv.x+fuzzy,tuv.y-fuzzy));' ,
    '       c+=texture2D(tex,vec2(tuv.x-fuzzy,tuv.y-fuzzy));' ,
    '   }' ,
    '   return c*0.2;' ,
    '}' ,
    'void main(){' ,
    '   vec2 vUv=gl_FragCoord.xy/resolution.xy;' ,
    '   vec4 texColor=vec4(0.);' ,
    '   if(fastMode){' ,
    '      texColor=blurFast(vUv,step,map,fastAxs);' ,
    '   }else{' ,
    '      texColor=blur(vUv,step,map);' ,
    '   }' ,

    '   gl_FragColor=texColor;' ,
    '}'
].join('\n');